Skip to content

fix(web): normalize Claude response viewer turns - #169

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
shenlvkang-collab:contrib/claude-response-viewer-normalization
Jul 28, 2026
Merged

fix(web): normalize Claude response viewer turns#169
Ark0N merged 2 commits into
Ark0N:masterfrom
shenlvkang-collab:contrib/claude-response-viewer-normalization

Conversation

@shenlvkang-collab

Copy link
Copy Markdown

Summary:\n- Rebind recovered sessions to an unambiguous Claude transcript UUID when persisted mux metadata is stale.\n- Build viewer cards at real human turn boundaries instead of rendering every JSONL content row as a separate message.\n- Deduplicate replayed assistant snapshots while preserving tool, task, skill, compact-summary, and team metadata filtering.\n- Add focused regression coverage for recovered sessions, turn grouping, and repeated streamed fragments.\n\nVerification:\n- test/routes/session-routes-claude-last-response.test.ts: 2 passed\n- typecheck, lint, frontend syntax, public asset checks, format check, lockfile check, and production build passed.\n\nThis branch is based on the current upstream master and contains only the Claude viewer normalization fix plus its changeset.

codeman-local and others added 2 commits July 25, 2026 15:26
Only CLAUDE.md conflicted: master restructured it into the short-rule +
docs/architecture-invariants.md pointer layout while this PR was open.
The response-viewer detail now lives in architecture-invariants, so the
Claude turn-grouping and restored-placeholder rebind notes moved there.
Changeset rewritten to record the measured effect on real transcripts.
@Ark0N

Ark0N commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Thanks, this one is a genuinely good diagnosis. I rebuilt it on current master, verified it against real transcripts rather than just the fixtures, and pushed the conflict resolution to your branch.

The diagnosis is right

The old code treated a Claude transcript as if it were a chat log, one row per message. It isn't: it's an append-only event log where a single logical exchange spans many rows. Tool results are recorded as user rows, assistant output arrives as multiple blocks sharing a message id, and restores replay earlier rows verbatim. Rendering a card per row was always going to produce duplicates and fragments, and the two extra things you caught are the ones that actually matter in practice:

  • Sidechain rows belong to subagents and forks, so the old parser could surface a subagent's output as the main session's "last response". Skipping isSidechain fixes a real correctness bug, not just cosmetics.
  • The restored-<uuid8> rebind. reconcileSessions() recovers a lost mux session with a stale cwd, so lookup by working directory finds nothing and the viewer just shows empty. Recognising that the placeholder still carries the first eight UUID characters, and only rebinding when exactly one candidate matches, is the right level of caution. The candidateIds.size !== 1 guard is what makes it safe.

Verified on real data

Fixtures prove the shape; they don't prove the fix helps on real transcripts or that grouping doesn't silently eat content. So I extracted both parsers verbatim, the master one and yours, and ran them over the 40 largest real transcripts on this machine:

master this PR
cards rendered 3108 621 (-80.0%)
duplicate cards 74 8
assistant text present in master but missing here 0 chars
last-response text differing (non-context=full) 0 of 40 files

The zero-character result is the important one: the grouping merges rows without dropping any assistant output. And the non-full path being byte-identical on every file means the plain last-response behaviour is a pure no-op, so the blast radius really is confined to the viewer.

I checked the 8 residual duplicates individually. All are legitimate: repeated "You've hit your session limit" notices and a repeated continue prompt, each separated by real turns. That is exactly the case your second test pins ("keeps an identical user prompt when it occurs again after an assistant response"), so the parser is behaving correctly, not leaking.

Worst individual case went from 282 cards to 18, and another from 184 (32 of them duplicates) to 68.

One thing worth being aware of

GET /api/sessions/:id/last-response now mutates session state, via adoptClaudeSessionId() plus a persistDockerCaseClaudeSessionId() write for docker cases. A GET with a side effect is normally a smell, and it will fire on every viewer poll for a recovered session.

I left it in, for three reasons: it only triggers on the restored- rebind path (a direct id match returns before it), adoptClaudeSessionId is idempotent so repeat polls converge after the first, and it mirrors what the hook path already does when it adopts a conversation id. Flagging it so it is a known and deliberate property rather than a surprise later.

Housekeeping

  • Merge conflict, CLAUDE.md only, and not your fault: master restructured it while this PR was open, moving deep detail into docs/architecture-invariants.md and leaving a short rule plus pointer behind. The response-viewer paragraph you edited now lives in architecture-invariants, so your Claude turn-grouping and restored-placeholder notes went there instead. Nothing was dropped.
  • Changeset rewritten to record the measured numbers, since it lands in the public CHANGELOG, and to drop the em dashes for repo style.

Verification

  • npm run test:ci: 187 files, 3786 passed, 12 skipped
  • tsc --noEmit, eslint, prettier --check, check:public-assets, check:frontend-syntax: clean
  • Both parsers compared over 40 real ~/.claude/projects transcripts as above

Thanks again. Reading the transcript format correctly instead of patching the symptom is the right instinct, and the sidechain and restored-placeholder catches were the parts that needed someone actually paying attention.

@Ark0N
Ark0N merged commit b586007 into Ark0N:master Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants